/**
 *    Copyright (C) 2007-2009 DIVIS GmbH (http://www.divis-hagen.de/)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 3 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General
 *    Public License along with this library; if not, write to the
 *    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *    Boston, MA 02110-1301 USA
 */
package de.divis_hagen.json;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 *
 * @author Aike J Sommer
 */
public class JSONParser {

    private PushbackReader in;

    public Map<String, Object> parse(Reader in) throws IOException, JSONException {
        this.in = new PushbackReader(in);
        return parseObject();
    }

    private Object parseValue() throws IOException, JSONException {
        int c = peek();
        if (c == '{') {
            return parseObject();
        } else if (c == '[') {
            return parseList();
        } else if (c == '\'' || c == '"') {
            return readString(true);
        } else {
            String value = readString(false);
            if ("null".equals(value)) {
                return null;
            }
            return value;
        }

//        throw new IllegalArgumentException("" + ((char) c));
    }

    private Map<String, Object> parseObject() throws IOException, JSONException {
        Map<String, Object> result = new HashMap<String, Object>();
        trim();
        require('{');
        trim();
        if (expect('}') == -1) {
            do {
                String name = readString(false);
                trim();
                require(':');
                trim();
                Object value = parseValue();
                result.put(name, value);
                trim();
            } while (expect(',') == ',');
            trim();
            require('}');
        }
        return result;
    }

    private List<Object> parseList() throws IOException, JSONException {
        List<Object> result = new ArrayList<Object>();
        trim();
        require('[');
        trim();
        if (expect(']') == -1) {
            do {
                Object value = parseValue();
                result.add(value);
                trim();
            } while (expect(',') == ',');
            trim();
            require(']');
        }
        return result;
    }

    private boolean isWhitespace(int c) {
        return c == ' ' || c == '\n' || c == '\r' || c == '\t';
    }

    private boolean isSeparator(int c) {
        return isWhitespace(c) || c == ',' || c == '{' || c == '}' || c == ':'
                || c == '[' || c == ']';
    }

    private void trim() throws IOException {
        int c;
        do {
            c = in.read();
        } while (isWhitespace(c));

        if (c != -1) {
            in.unread(c);
        }
    }

    private int expect(int... expected) throws IOException {
        int c = in.read();
        for (int i = 0; i < expected.length; i++) {
            int j = expected[i];
            if (j == c) {
                return c;
            }
        }

        in.unread(c);
        return -1;
    }

    private int peek() throws IOException {
        int c = in.read();
        in.unread(c);
        return c;
    }

    private int require(int... expected) throws JSONException, IOException {
        int found = expect(expected);
        if (found == -1) {
            throw new JSONException("expected: {" + implode(expected, ", ") + "} - found: " + ((char) peek()));
        }
        return found;
    }

    private String implode(int[] chars, String spacer) {
        StringBuilder s = null;
        for (int i = 0; i < chars.length; i++) {
            int c = chars[i];
            if (s == null) {
                s = new StringBuilder("" + ((char) c));
            } else {
                s.append(spacer);
                s.append((char) c);
            }
        }
        return s == null ? "" : s.toString();
    }

    private String readString(boolean strict) throws JSONException, IOException {
        int quote;
        if (strict) {
            quote = require('"', '\'');
        } else {
            quote = expect('"', '\'');
        }

        StringBuilder result = new StringBuilder();
        int c = in.read();
        while (c != -1) {
            if (quote == -1 && isSeparator(c)) {
                in.unread(c);
                break;
            } else if (c == quote) {
                break;
            } else if (c == '\\') {
                c = special();
            }

            result.append((char)c);

            c = in.read();
        }

        return result.toString();
    }

    private int special() throws IOException {
        int c = in.read();
        switch (c) {
            case '\\':
            case '\'':
            case '"':
            case ' ':
                return c;
            default:
                throw new IllegalArgumentException("" + ((char)c));
        }
    }

}
